// MIT License
//
// Copyright (c) 2017-2020 MessageKit
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import InputBarAccessoryView
import Kingfisher
import MapKit
import MessageKit
import UIKit

// MARK: - AdvancedExampleViewController

final class AdvancedExampleViewController: ChatViewController {
  // MARK: Public

  // MARK: - UICollectionViewDataSource

  public override func collectionView(
    _ collectionView: UICollectionView,
    cellForItemAt indexPath: IndexPath)
    -> UICollectionViewCell
  {
    guard let messagesDataSource = messagesCollectionView.messagesDataSource else {
      fatalError("Ouch. nil data source for messages")
    }

    // Very important to check this when overriding `cellForItemAt`
    // Super method will handle returning the typing indicator cell
    guard !isSectionReservedForTypingIndicator(indexPath.section) else {
      return super.collectionView(collectionView, cellForItemAt: indexPath)
    }

    let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView)
    if case .custom = message.kind {
      let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath)
      cell.configure(with: message, at: indexPath, and: messagesCollectionView)
      return cell
    }
    return super.collectionView(collectionView, cellForItemAt: indexPath)
  }

  // MARK: Internal

  let outgoingAvatarOverlap: CGFloat = 17.5

  override func viewDidLoad() {
    messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout())
    messagesCollectionView.register(CustomCell.self)
    super.viewDidLoad()

    updateTitleView(title: "MessageKit", subtitle: "2 Online")
  }

  override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)

    MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu])
      .onTypingStatus { [weak self] in
        self?.setTypingIndicatorViewHidden(false)
      }.onNewMessage { [weak self] message in
        self?.setTypingIndicatorViewHidden(true, performUpdates: {
          self?.insertMessage(message)
        })
      }
  }

  override func loadFirstMessages() {
    DispatchQueue.global(qos: .userInitiated).async {
      let count = UserDefaults.standard.mockMessagesCount()
      SampleData.shared.getAdvancedMessages(count: count) { messages in
        DispatchQueue.main.async {
          self.messageList = messages
          self.messagesCollectionView.reloadData()
          self.messagesCollectionView.scrollToLastItem()
        }
      }
    }
  }

  override func loadMoreMessages() {
    DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) {
      SampleData.shared.getAdvancedMessages(count: 20) { messages in
        DispatchQueue.main.async {
          self.messageList.insert(contentsOf: messages, at: 0)
          self.messagesCollectionView.reloadDataAndKeepOffset()
          self.refreshControl.endRefreshing()
        }
      }
    }
  }

  override func configureMessageCollectionView() {
    super.configureMessageCollectionView()

    let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout
    layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8)

    // Hide the outgoing avatar and adjust the label alignment to line up with the messages
    layout?.setMessageOutgoingAvatarSize(.zero)
    layout?
      .setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(
        textAlignment: .right,
        textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))
    layout?
      .setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(
        textAlignment: .right,
        textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8)))

    // Set outgoing avatar to overlap with the message bubble
    layout?
      .setMessageIncomingMessageTopLabelAlignment(LabelAlignment(
        textAlignment: .left,
        textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0)))
    layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30))
    layout?
      .setMessageIncomingMessagePadding(UIEdgeInsets(
        top: -outgoingAvatarOverlap,
        left: -18,
        bottom: outgoingAvatarOverlap,
        right: 18))

    layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30))
    layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0))
    layout?.setMessageIncomingAccessoryViewPosition(.messageBottom)
    layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30))
    layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8))

    messagesCollectionView.messagesLayoutDelegate = self
    messagesCollectionView.messagesDisplayDelegate = self
  }

  override func configureMessageInputBar() {
    // super.configureMessageInputBar()

    messageInputBar = CameraInputBarAccessoryView()
    messageInputBar.delegate = self
    messageInputBar.inputTextView.tintColor = .primaryColor
    messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal)
    messageInputBar.sendButton.setTitleColor(
      UIColor.primaryColor.withAlphaComponent(0.3),
      for: .highlighted)

    messageInputBar.isTranslucent = true
    messageInputBar.separatorLine.isHidden = true
    messageInputBar.inputTextView.tintColor = .primaryColor
    messageInputBar.inputTextView.backgroundColor = UIColor(red: 245 / 255, green: 245 / 255, blue: 245 / 255, alpha: 1)
    messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1)
    messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36)
    messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36)
    messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1).cgColor
    messageInputBar.inputTextView.layer.borderWidth = 1.0
    messageInputBar.inputTextView.layer.cornerRadius = 16.0
    messageInputBar.inputTextView.layer.masksToBounds = true
    messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0)
    configureInputBarItems()
    inputBarType = .custom(messageInputBar)
  }

  // MARK: - Helpers

  func isTimeLabelVisible(at indexPath: IndexPath) -> Bool {
    indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath)
  }

  func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool {
    guard indexPath.section - 1 >= 0 else { return false }
    return messageList[indexPath.section].user == messageList[indexPath.section - 1].user
  }

  func isNextMessageSameSender(at indexPath: IndexPath) -> Bool {
    guard indexPath.section + 1 < messageList.count else { return false }
    return messageList[indexPath.section].user == messageList[indexPath.section + 1].user
  }

  func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) {
    updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...")
    setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in
      if success, self?.isLastSectionVisible() == true {
        self?.messagesCollectionView.scrollToLastItem(animated: true)
      }
    }
  }

  // MARK: - MessagesDataSource

  override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
    if isTimeLabelVisible(at: indexPath) {
      return NSAttributedString(
        string: MessageKitDateFormatter.shared.string(from: message.sentDate),
        attributes: [
          NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
          NSAttributedString.Key.foregroundColor: UIColor.darkGray,
        ])
    }
    return nil
  }

  override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
    if !isPreviousMessageSameSender(at: indexPath) {
      let name = message.sender.displayName
      return NSAttributedString(
        string: name,
        attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
    }
    return nil
  }

  override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
    if !isNextMessageSameSender(at: indexPath), isFromCurrentSender(message: message) {
      return NSAttributedString(
        string: "Delivered",
        attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
    }
    return nil
  }

  // MARK: Private

  private func configureInputBarItems() {
    messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false)
    messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
    messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2)
    messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false)
    messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up")
    messageInputBar.sendButton.title = nil
    messageInputBar.sendButton.imageView?.layer.cornerRadius = 16
    let charCountButton = InputBarButtonItem()
      .configure {
        $0.title = "0/140"
        $0.contentHorizontalAlignment = .right
        $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal)
        $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold)
        $0.setSize(CGSize(width: 50, height: 25), animated: false)
      }.onTextViewDidChange { item, textView in
        item.title = "\(textView.text.count)/140"
        let isOverLimit = textView.text.count > 140
        item.inputBarAccessoryView?
          .shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit
        if isOverLimit {
          item.inputBarAccessoryView?.sendButton.isEnabled = false
        }
        let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1)
        item.setTitleColor(color, for: .normal)
      }
    let bottomItems = [.flexibleSpace, charCountButton]

    configureInputBarPadding()

    messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false)

    // This just adds some more flare
    messageInputBar.sendButton
      .onEnabled { item in
        UIView.animate(withDuration: 0.3, animations: {
          item.imageView?.backgroundColor = .primaryColor
        })
      }.onDisabled { item in
        UIView.animate(withDuration: 0.3, animations: {
          item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1)
        })
      }
  }

  /// The input bar will autosize based on the contained text, but we can add padding to adjust the height or width if necessary
  /// See the InputBar diagram here to visualize how each of these would take effect:
  /// https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/InputBarAccessoryViewLayout.png
  private func configureInputBarPadding() {
    // Entire InputBar padding
    messageInputBar.padding.bottom = 8

    // or MiddleContentView padding
    messageInputBar.middleContentViewPadding.right = -38

    // or InputTextView padding
    messageInputBar.inputTextView.textContainerInset.bottom = 8
  }

  private func makeButton(named: String) -> InputBarButtonItem {
    InputBarButtonItem()
      .configure {
        $0.spacing = .fixed(10)
        $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate)
        $0.setSize(CGSize(width: 25, height: 25), animated: false)
        $0.tintColor = UIColor(white: 0.8, alpha: 1)
      }.onSelected {
        $0.tintColor = .primaryColor
      }.onDeselected {
        $0.tintColor = UIColor(white: 0.8, alpha: 1)
      }.onTouchUpInside {
        print("Item Tapped")
        let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        actionSheet.addAction(action)
        if let popoverPresentationController = actionSheet.popoverPresentationController {
          popoverPresentationController.sourceView = $0
          popoverPresentationController.sourceRect = $0.frame
        }
        self.navigationController?.present(actionSheet, animated: true, completion: nil)
      }
  }
}

// MARK: MessagesDisplayDelegate

extension AdvancedExampleViewController: MessagesDisplayDelegate {
  // MARK: - Text Messages

  func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
    isFromCurrentSender(message: message) ? .white : .darkText
  }

  func detectorAttributes(
    for detector: DetectorType,
    and message: MessageType,
    at _: IndexPath) -> [NSAttributedString.Key: Any]
  {
    switch detector {
    case .hashtag, .mention:
      if isFromCurrentSender(message: message) {
        return [.foregroundColor: UIColor.white]
      } else {
        return [.foregroundColor: UIColor.primaryColor]
      }
    default: return MessageLabel.defaultAttributes
    }
  }

  func enabledDetectors(for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] {
    [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag]
  }

  // MARK: - All Messages

  func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
    isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230 / 255, green: 230 / 255, blue: 230 / 255, alpha: 1)
  }

  func messageStyle(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageStyle {
    var corners: UIRectCorner = []

    if isFromCurrentSender(message: message) {
      corners.formUnion(.topLeft)
      corners.formUnion(.bottomLeft)
      if !isPreviousMessageSameSender(at: indexPath) {
        corners.formUnion(.topRight)
      }
      if !isNextMessageSameSender(at: indexPath) {
        corners.formUnion(.bottomRight)
      }
    } else {
      corners.formUnion(.topRight)
      corners.formUnion(.bottomRight)
      if !isPreviousMessageSameSender(at: indexPath) {
        corners.formUnion(.topLeft)
      }
      if !isNextMessageSameSender(at: indexPath) {
        corners.formUnion(.bottomLeft)
      }
    }

    return .custom { view in
      let radius: CGFloat = 16
      let path = UIBezierPath(
        roundedRect: view.bounds,
        byRoundingCorners: corners,
        cornerRadii: CGSize(width: radius, height: radius))
      let mask = CAShapeLayer()
      mask.path = path.cgPath
      view.layer.mask = mask
    }
  }

  func configureAvatarView(
    _ avatarView: AvatarView,
    for message: MessageType,
    at indexPath: IndexPath,
    in _: MessagesCollectionView)
  {
    let avatar = SampleData.shared.getAvatarFor(sender: message.sender)
    avatarView.set(avatar: avatar)
    avatarView.isHidden = isNextMessageSameSender(at: indexPath)
    avatarView.layer.borderWidth = 2
    avatarView.layer.borderColor = UIColor.primaryColor.cgColor
  }

  func configureAccessoryView(_ accessoryView: UIView, for _: MessageType, at _: IndexPath, in _: MessagesCollectionView) {
    // Cells are reused, so only add a button here once. For real use you would need to
    // ensure any subviews are removed if not needed
    accessoryView.subviews.forEach { $0.removeFromSuperview() }
    accessoryView.backgroundColor = .clear

    let shouldShow = Int.random(in: 0 ... 10) == 0
    guard shouldShow else { return }

    let button = UIButton(type: .infoLight)
    button.tintColor = .primaryColor
    accessoryView.addSubview(button)
    button.frame = accessoryView.bounds
    button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate`
    accessoryView.layer.cornerRadius = accessoryView.frame.height / 2
    accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3)
  }

  func configureMediaMessageImageView(
    _ imageView: UIImageView,
    for message: MessageType,
    at _: IndexPath,
    in _: MessagesCollectionView)
  {
    if case MessageKind.photo(let media) = message.kind, let imageURL = media.url {
      imageView.kf.setImage(with: imageURL)
    } else {
      imageView.kf.cancelDownloadTask()
    }
  }

  // MARK: - Location Messages

  func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MKAnnotationView? {
    let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil)
    let pinImage = #imageLiteral(resourceName: "ic_map_marker")
    annotationView.image = pinImage
    annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2)
    return annotationView
  }

  func animationBlockForLocation(
    message _: MessageType,
    at _: IndexPath,
    in _: MessagesCollectionView) -> ((UIImageView) -> Void)?
  {
    { view in
      view.layer.transform = CATransform3DMakeScale(2, 2, 2)
      UIView.animate(
        withDuration: 0.6,
        delay: 0,
        usingSpringWithDamping: 0.9,
        initialSpringVelocity: 0,
        options: [],
        animations: {
          view.layer.transform = CATransform3DIdentity
        },
        completion: nil)
    }
  }

  func snapshotOptionsForLocation(
    message _: MessageType,
    at _: IndexPath,
    in _: MessagesCollectionView)
    -> LocationMessageSnapshotOptions
  {
    LocationMessageSnapshotOptions(
      showsBuildings: true,
      showsPointsOfInterest: true,
      span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10))
  }

  // MARK: - Audio Messages

  func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor {
    isFromCurrentSender(message: message) ? .white : .primaryColor
  }

  func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) {
    audioController
      .configureAudioCell(
        cell,
        message: message) // this is needed especially when the cell is reconfigure while is playing sound
  }
}

// MARK: MessagesLayoutDelegate

extension AdvancedExampleViewController: MessagesLayoutDelegate {
  func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
    if isTimeLabelVisible(at: indexPath) {
      return 18
    }
    return 0
  }

  func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
    if isFromCurrentSender(message: message) {
      return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0
    } else {
      return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0
    }
  }

  func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat {
    (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0
  }
}

// MARK: CameraInputBarAccessoryViewDelegate

extension AdvancedExampleViewController: CameraInputBarAccessoryViewDelegate {
  func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment]) {
    for item in attachments {
      if case .image(let image) = item {
        self.sendImageMessage(photo: image)
      }
    }
    inputBar.invalidatePlugins()
  }

  func sendImageMessage(photo: UIImage) {
    let photoMessage = MockMessage(image: photo, user: currentSender as! MockUser, messageId: UUID().uuidString, date: Date())
    insertMessage(photoMessage)
  }
}
